package groep1.graph;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import bluej.debugmgr.inspector.FieldInfo;

/**
 * An object which can be displayed on the graph. The characteristics of an
 * object are the following: id, packageName, className, objectName. These
 * should all be present in the full object name.
 * 
 * @author Niels Billen, Jorn Van Loock
 * @version 0.2
 * @since 0.1
 */
public class GraphObject {
	/**
	 * Creates a new object to display on the graph.
	 * 
	 * @param fullObjectName
	 *            The full name of the object: id:package.class.fieldname
	 * @param fieldList
	 *            The list with the info about the fields of the object.
	 */
	public GraphObject(String fullObjectName, List<FieldInfo> fieldInfo) {
		ArrayList<String> individualNames = nameParser(fullObjectName); //TODO: andere klasse?
		setObjectName(individualNames);
		setFieldInfo(fieldInfo);
	}

	/**
	 * Sets the individual names of the object.
	 * 
	 * @param individualNames
	 *            The individual names of the object.
	 * @throws NullPointerException
	 *             When the given names are null.
	 * @throws IllegalArgumentException
	 *             When the individual names do not have enough components.
	 */
	private void setObjectName(ArrayList<String> individualNames) {
		if (individualNames == null)
			throw new NullPointerException(
					"The given individual names are null!");
		//if (individualNames.size() < 4)
		//	throw new IllegalArgumentException(
		//			"Not all the components of a name are present!");
		setId(individualNames.get(0)); // sets the id of the object
		individualNames.remove(0); // remove id
		setName(individualNames.get(individualNames.size() - 1)); // sets the
																	// fieldname
		individualNames.remove(individualNames.size() - 1); // remove fieldname
		setClassName(individualNames.get(individualNames.size() - 1)); // sets
																		// the
																		// classname
		individualNames.remove(individualNames.size() - 1); // remove classname
		String fullPackageName = new String();
		for (String packageName : individualNames)
			fullPackageName += packageName + ".";
		setPackage(fullPackageName); // set packagename
	}

	/**
	 * Sets the id of the object.
	 * 
	 * @param id
	 *            The id of the object.
	 * @throws NullPointerException
	 *             When the given id is null.
	 */
	private void setId(String id) {
		if (id == null)
			throw new NullPointerException("The given name is null!");
		this.id = id;
	}

	/**
	 * Sets the package of the object.
	 * 
	 * @param className
	 *            The package of the object.
	 * @throws NullPointerException
	 *             When the given package name is null.
	 */
	private void setPackage(String fullPackageName) {
		if (fullPackageName == null)
			throw new NullPointerException("The given name is null!");
		this.packageName = fullPackageName;
	}

	/**
	 * Sets the class of the object.
	 * 
	 * @param className
	 *            The class of the object.
	 * @throws NullPointerException
	 *             When the given class name is null.
	 */
	private void setClassName(String className) {
		if (className == null)
			throw new NullPointerException("The given name is null!");
		this.className = className;
	}

	/**
	 * Sets the name of the object.
	 * 
	 * @param objectName
	 *            The name of the object.
	 * @throws NullPointerException
	 *             When the given object name is null.
	 */
	private void setName(String objectName) {
		if (objectName == null)
			throw new NullPointerException("The given name is null!");
		this.objectName = objectName;
	}

	/**
	 * Returns an array of Strings of the parsed fullname
	 * [id,package1,package2,..., class, fieldname]
	 * 
	 * @param fullObjectName
	 *            The fullObjectName. e.g: "id:package1.package2.class.field"
	 * 
	 * @return an array of Strings of the parsed fullname
	 */
	protected ArrayList<String> nameParser(String fullObjectName) {
		String delimiters = "[:.]"; // Delimiters
		String[] tokens = fullObjectName.split(delimiters, 0);
		ArrayList<String> list = new ArrayList<String>();
		for (String tempToken : tokens)
			list.add(tempToken);
		return list;
	}

	/**
	 * Returns the id of the object.
	 * 
	 * @return the id of the object.
	 */
	public String getId() {
		return id;
	}

	/**
	 * The id of the object.
	 */
	private String id;

	/**
	 * Returns the package of the object.
	 * 
	 * @return the package of the object.
	 */
	public String getPackage() {
		return packageName;
	}

	/**
	 * The package of the object.
	 */
	private String packageName;

	/**
	 * Returns the class of the object.
	 * 
	 * @return the class of the object.
	 */
	public String getClassName() {
		return className;
	}

	/**
	 * The class of the object.
	 */
	private String className;

	/**
	 * Returns the name of the object.
	 * 
	 * @return the name of the object.
	 */
	public String getObjectName() {
		return objectName;
	}

	/**
	 * The name of the object.
	 */
	private String objectName;

	/**
	 * Adds a field to the fields.
	 * 
	 * @param fieldInfo
	 *            The info to add to the fields.
	 * @throws NullPointerException
	 *             When the given field info is null.
	 */
	public void addFieldInfo(FieldInfo fieldInfo) {
		if (fieldInfo == null)
			throw new NullPointerException("The given field info is null!");
		this.fieldInfo.add(fieldInfo);
	}

	/**
	 * Sets the field with the info about the fields.
	 * 
	 * @param fieldInfo
	 *            The info about the fields.
	 * @throws NullPointerException
	 *             When the given field info is null.
	 */
	public void setFieldInfo(List<FieldInfo> fieldInfo)
			throws NullPointerException {
		if (fieldInfo == null)
			throw new NullPointerException("The given field info is null!");
		this.fieldInfo.clear();
		this.fieldInfo.ensureCapacity(fieldInfo.size());
		this.fieldInfo.addAll(fieldInfo);
	}

	/**
	 * Returns the list with the info about the fields of the object.
	 * 
	 * @return the list with the info about the fields of the object.
	 */
	public List<FieldInfo> getFieldInfo() {
		return new ArrayList<FieldInfo>(fieldInfo);
	}

	/**
	 * Returns the width of this graph object.
	 * 
	 * @return the width of this graph object.
	 */
	public int getWidth() {
		int nameWidth = getStringSize(getObjectName(), headerFont).width
				+ horizontalOffset * 2;

		int classWidth = getStringSize("Class: " + getClassName(),
				identificationFont).width + horizontalOffset * 2;
		int idWidth = getStringSize("Id: " + getId(), identificationFont).width
				+ horizontalOffset * 2;
		int packageWidth = getStringSize("Package: " + getPackage(),
				identificationFont).width + horizontalOffset * 2;

		int maximumHeadingWidth = Math.max(nameWidth,
				Math.max(classWidth, Math.max(idWidth, packageWidth)));

		int maximumDescription = getWidthOfLongestDescription();
		int maximumValue = getWidthOfLongestValue();

		return Math.max(maximumHeadingWidth, fieldOffset + maximumDescription
				+ fieldValueSeperation + maximumValue + valueOffset);
	}

	/**
	 * Returns the width of the longest description.
	 * 
	 * @return the width of the longest description.
	 */
	protected int getWidthOfLongestDescription() {
		int result = 0;

		for (FieldInfo info : fieldInfo)
			result = Math
					.max(result,
							getStringSize(info.getDescription(),
									descriptionFont).width);

		return result;
	}

	/**
	 * Returns the width of the longest value.
	 * 
	 * @return the width of the longest value.
	 */
	protected int getWidthOfLongestValue() {
		int result = 0;

		for (FieldInfo info : fieldInfo)
			result = Math.max(result,
					getStringSize(info.getValue(), valueFont).width);

		return result;
	}

	/**
	 * Returns the height of this graph object.<br>
	 * <br>
	 * A graph object is constructed as following:<br>
	 * <br>
	 * <code>
	 * |________________________|<br>
	 * |-----verticaloffset-----|<br>
	 * |------object name-------|<br>
	 * |-----verticaloffset-----|<br>
	 * |---------class----------|<br>
	 * |-----verticaloffset-----|<br>
	 * |---------package--------|<br>
	 * |-----verticaloffset-----|<br>
	 * |-----------id-----------|<br>
	 * |-----verticaloffset-----|<br>
	 * |________________________|<br>
	 * |-----fieldseperation----|<br>
	 * |---------field----------|<br>
	 * |-----fieldseperation----|<br>
	 * |________________________|</code>
	 * 
	 * @return the height of this graph object.
	 */
	public int getHeight() {
		int height = headerFont.getSize() + verticalOffset * 3 + 2
				* verticalIdentificationSeperation
				+ identificationFont.getSize() * 3 + verticalFieldSeperation;

		height += fieldInfo.size()
				* (Math.max(headerFont.getSize(), descriptionFont.getSize()) + verticalFieldSeperation);

		return height;
	}

	/**
	 * Sets the position of this graph object to the given position.
	 * 
	 * @param x
	 *            The x coordinate.
	 * @param y
	 *            The y coordinate.
	 */
	public void setPosition(int x, int y) {
		position.setLocation(x, y);
	}

	/**
	 * Returns the position of this graph object.
	 * 
	 * @return the position of this graph object.
	 */
	public Point getPosition() {
		return new Point(position);
	}

	/**
	 * The position of this graph object.
	 */
	private Point position = new Point();

	/**
	 * Returns the absolute referer Point on the left side of this graph object.
	 * 
	 * @return the absolute referer Point on the left side of this graph object.
	 */
	public Point getIncomingCoordinate() {
		Dimension headerSize = getStringSize(getObjectName(), headerFont);

		return new Point((int) getPosition().getX(), (int) getPosition().getY()
				+ (verticalOffset * 2 + headerSize.height) / 2);
	}

	/**
	 * Returns whether the given point is inside this object.
	 * 
	 * @param point
	 *            The point to check.
	 * @return whether the given point is inside this object.
	 */
	public boolean overlap(Point point) {
		if (point == null)
			return false;
		if (point.x < getPosition().x || point.x > getPosition().x + getWidth())
			return false;
		if (point.y < getPosition().y
				|| point.y > getPosition().y + getHeight())
			return false;
		return true;
	}

	/**
	 * Returns the absolute referer Point on the right side of this graph object
	 * for the given field info.
	 * 
	 * @param fieldInfo
	 *            The field info of the object.
	 * @return the absolute referer Point on the right side.
	 * 
	 * @throws NullPointerException
	 *             When teh given field is null.
	 * @throws NullPointerException
	 *             When the given field is not valid.
	 */
	public Point getOutgoingCoordinateOfField(FieldInfo info) {
		if (info == null)
			throw new NullPointerException("The given field is null!");
		return getOutgoingCoordinateOfField(info.getDescription());
	}

	/**
	 * Returns the absolute referer Point on the right side of this graph object
	 * for a given fieldName.<br>
	 * 
	 * <code>
	 * |________________________|<br>
	 * |-----verticaloffset-----|<br>
	 * |------object name-------|<br>
	 * |-----verticaloffset-----|<br>
	 * |---------class----------|<br>
	 * |-----verticaloffset-----|<br>
	 * |---------package--------|<br>
	 * |-----verticaloffset-----|<br>
	 * |-----------id-----------|<br>
	 * |-----verticaloffset-----|<br>
	 * |________________________|<br>
	 * |-----fieldseperation----|<br>
	 * |---------field----------|<br>
	 * |-----fieldseperation----|<br>
	 * |________________________|</code>
	 * 
	 * @param String
	 *            a fieldName of the object
	 * 
	 * @return the absolute referer Point on the right side.
	 * 
	 * @throws NullPointerException
	 *             When the given field is not valid.
	 */
	public Point getOutgoingCoordinateOfField(String fieldName)
			throws NullPointerException {
		int fieldNumber = -1;
		for (int i = 0; i < getFieldInfo().size(); i++) {
			if ((getFieldInfo().get(i).getDescription()).equals(fieldName)) {
				fieldNumber = i;
				break;
			}
		}
		if (fieldNumber == -1)
			throw new NullPointerException("Field not found!");
		int maxSize = Math.max(descriptionFont.getSize(), valueFont.getSize());
		int headerFontHeight = headerFont.getSize();
		int indentificationFontHeight = identificationFont.getSize();

		double relativex = getWidth();
		double relativey = 3 * verticalOffset + 2
				* verticalIdentificationSeperation + (fieldNumber + 1)
				* verticalFieldSeperation + (fieldNumber+0.5) * maxSize
				+ headerFontHeight + 3 * indentificationFontHeight;
		System.out.println(fieldName);
		System.out.println(relativey);
		return new Point((int) (getPosition().getX() + relativex),
				(int) (getPosition().getY() + relativey));
	}

	/**
	 * Returns the size of a given string drawn in the given font.
	 * 
	 * @param string
	 *            The string to get the size of.
	 * @throws NullPointerException
	 *             When the given string is null.
	 */
	public Dimension getStringSize(String string, Font font)
			throws NullPointerException {
		if (string == null)
			throw new NullPointerException("The given string is null!");

		/*
		 * Create a dummy image to get a Graphics object and a FontMetrics
		 * object. The width of a String can only be measured correctly by
		 * checking out the object where it needs to be drawn upon.
		 */
		BufferedImage image = new BufferedImage(16, 16,
				BufferedImage.TYPE_INT_ARGB);
		Graphics g = image.getGraphics();
		FontMetrics metrics = g.getFontMetrics(font);
		Rectangle2D size = metrics.getStringBounds(string, g);

		return new Dimension((int) size.getWidth(), (int) size.getHeight());
	}

	/**
	 * Draws this GraphObject on the given graphics at the given position.
	 * 
	 * @param graphics
	 *            The graphics to draw upon.
	 * @param x
	 *            The x position to draw this object on.
	 * @param y
	 *            The y position to draw this object on.
	 */
	public void draw(Graphics graphics, int xOffset, int yOffset)
			throws NullPointerException {
		if (graphics == null)
			throw new NullPointerException("The given graphics are null!");

		int x = getPosition().x + xOffset;
		int y = getPosition().y + yOffset;
		int width = getWidth();
		int height = getHeight();

		// Draws the rounded rectangle
		graphics.setColor(fill);
		graphics.fillRoundRect(x, y, width, height, 8, 8);
		graphics.setColor(outline);
		graphics.drawRoundRect(x, y, width, height, 8, 8);

		// Draw the name of the object.
		Dimension stringSize = getStringSize(getObjectName(), headerFont);
		int drawY = y + headerFont.getSize() + verticalOffset;
		graphics.setFont(headerFont);
		graphics.drawString(getObjectName(),
				x + (width - stringSize.width) / 2, drawY);

		// Draw the classname of the object.
		stringSize = getStringSize("Class: " + getClassName(),
				identificationFont);
		drawY += identificationFont.getSize() + verticalOffset;
		graphics.setFont(identificationFont);
		graphics.drawString("Class: " + getClassName(), x
				+ (width - stringSize.width) / 2, drawY);

		// Draw the packagename of the object.
		stringSize = getStringSize("Packege: " + getPackage(),
				identificationFont);
		drawY += identificationFont.getSize()
				+ verticalIdentificationSeperation;
		graphics.setFont(identificationFont);
		graphics.drawString("Package: " + getPackage(), x
				+ (width - stringSize.width) / 2, drawY);

		// Draw the id of the object.
		stringSize = getStringSize("Id: " + getId(), identificationFont);
		drawY += identificationFont.getSize()
				+ verticalIdentificationSeperation;
		graphics.setFont(identificationFont);
		graphics.drawString("Id: " + getId(), x + (width - stringSize.width)
				/ 2, drawY);

		drawY += verticalOffset;
		graphics.drawLine(x, drawY, x + getWidth(), drawY);

		// Draw the fields
		FieldInfo info;
		int maxSize = Math.max(descriptionFont.getSize(), valueFont.getSize());
		drawY += verticalFieldSeperation + maxSize;

		for (int i = 0; i < getFieldInfo().size(); i++) {
			info = getFieldInfo().get(i);
			graphics.setFont(descriptionFont);
			graphics.drawString(info.getDescription(), x + horizontalOffset,
					drawY);
			graphics.setFont(valueFont);
			stringSize = getStringSize(info.getValue(), descriptionFont);
			graphics.drawString(info.getValue(), x + width - valueOffset
					- stringSize.width, drawY);
			drawY += maxSize + verticalFieldSeperation;
		}
	}

	/**
	 * The color of the outline.
	 */
	protected Color outline = Color.BLACK;

	/**
	 * The color of the graph object.
	 */
	protected Color fill = Color.GRAY;

	/**
	 * The font for the name of the object.
	 */
	protected Font headerFont = new Font("Helvetica", 1, 14);

	/**
	 * The font for the identification of the object. id, class & package
	 */
	protected Font identificationFont = new Font("Helvetica", 1, 10);

	/**
	 * The font for the field description of this object.
	 */
	protected Font descriptionFont = new Font("Helvetica", 0, 12);

	/**
	 * The font for the field value of this object.
	 */
	protected Font valueFont = new Font("Helvetica", 0, 12);

	/**
	 * The horizontal offset in pixels of the objects name.<br>
	 * <br>
	 * This field depicts the minimum seperation between the box and name of the
	 * object.
	 * 
	 * <code>|----(offset)----ObjectName----(offset)----|</code>
	 */
	protected int horizontalOffset = 8;

	/**
	 * The vertical offset in pixels of the objects name.<br>
	 * <br>
	 * This field depicts the minimum seperation between the box and name of the
	 * object.<br>
	 * <br>
	 * <code>|__________________________|</code><br>
	 * <code>|----------Offset----------|</code><br>
	 * <code>|--------ObjectName--------|</code><br>
	 * <code>|----------Offset----------|</code><br>
	 */
	protected int verticalOffset = 8;

	/**
	 * The vertical seperation the identificationtags.
	 */
	protected int verticalIdentificationSeperation = 4;

	/**
	 * The vertical seperation between fields.
	 */
	protected int verticalFieldSeperation = 8;

	/**
	 * The horizontal offset of the field against the box.
	 * 
	 * <code>|--(Offset)--FieldDescription------Value---|</code>
	 */
	protected int fieldOffset = 8;

	/**
	 * The seperation between a field and it's value.
	 */
	protected int fieldValueSeperation = 16;

	/**
	 * The offset of a value from the right side of the box.
	 * 
	 * <code>|----FieldDescription------Value-(Offset)--|</code>
	 */
	protected int valueOffset = 8;

	/**
	 * The info about the fields.
	 */
	public ArrayList<FieldInfo> fieldInfo = new ArrayList<FieldInfo>();

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "[GraphObject] with name " + getObjectName() + " and position "
				+ getPosition() + " and width " + getWidth() + " and height "
				+ getHeight() + ".";
	}
}
